v8 引擎 JS 代码是如何执行的
在执行一段代码时,JS 引擎会首先创建一个执行栈。然后 JS 引擎会创建一个全局执行上下文,并 push 到执行栈中, 这个过程 JS 引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,JS 引擎会进入执行阶段,这个过程 JS 引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值)。
如果这段代码中存在 function 的声明和调用,那么 JS 引擎会创建一个函数执行上下文,并 push 到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,JS 引擎会在父函数执行的过程中,将子函数的全局执行上下文 push 到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。
还有一种特殊情况是,在子函数执行的过程中,父函数已经 return 了,这种情况下,JS 引擎会将父函数的上下文从执行栈中移除,与此同时,JS 引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,JS 引擎才会将子函数的上下文及闭包一并从执行栈中移除。
为什么要设计成单线程?
浏览器端的脚本主要的任务就是处理用户的交互,而用户的交互无非就是响应 DOM 上的一些事件/增删改 DOM 中的元素。多线程会带来很复杂的同步问题。
JS 运行机制
js 将所有任务分成两种,一种是同步任务,另一种是异步任务。在所有同步任务执行完之前,任何的异步任务是不会执行的。一般来说,有以下四种会放入异步任务队列:
- setTimeout 和 setInterval
- DOM 事件
- ES6 中的 Promise
- ajax 异步请求
Event Loop
宏任务、微任务
浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。
- 常见的 macro-task 比如:setTimeout、setInterval、setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
- 常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5 新特性)、process.nextTick 等。
微任务
process.nextTickpromiseObject.observeMutationObserver
宏任务
scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。
浏览器中的 Event Loop
异步事件会被放置到对应的宏任务队列或者微任务队列中去,当执行栈为空的时候,主线程会首先查看微任务中的事件,如果微任务不是空的那么执行微任务中的事件,如果没有,则在宏任务中取出最前面的一个事件。把对应的回调加入当前执行栈...如此反复,进入循环。
浏览器中的 Event Loop 执行顺序:
- 首先执行同步代码,这属于宏任务
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有必要会渲染页面
- 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是
setTimeout中的回调函数
Node 中的 Event Loop
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
日常开发中的绝大部分异步任务都是在timers、poll、check这 3 个阶段处理的。
1. timer
timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。
2. I/O
I/O 阶段会处理一些上一轮循环中的少数未执行的 I/O 回调
3. idle, prepare
idle, prepare 阶段内部实现,这里就忽略不讲了。
4. poll
poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情
- 回到 timer 阶段执行回调
- 执行 I/O 回调
并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情
- 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
- 如果 poll 队列为空时,会有两件事发生
- 如果有
setImmediate回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调 - 如果没有
setImmediate回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
- 如果有
当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。
5. check
check 阶段执行 setImmediate
setImmediate() 的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后。 我们先来看个例子:
console.log("start");
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function () {
console.log("promise1");
});
}, 0);
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function () {
console.log("promise2");
});
}, 0);
Promise.resolve().then(function () {
console.log("promise3");
});
console.log("end");
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
- 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出 promise3
- 然后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 micro-task 队列,同样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于 Node 与浏览器的 Event Loop 差异,下文还会详细介绍)。
6. close callbacks
close callbacks 阶段执行 close 事件
在以上的内容中,我们了解了 Node 中的 Event Loop 的执行顺序,接下来我们将会通过代码的方式来深入理解这块内容。
首先在有些情况下,定时器的执行顺序其实是随机的
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
对于以上代码来说,setTimeout 可能执行在前,也可能执行在后
- 首先
setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的 - 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行
setTimeout回调 - 那么如果准备时间花费小于 1ms,那么就是
setImmediate回调先执行了
当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
});
在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。
上面介绍的都是 macro-task 的执行情况,对于 micro-task 来说,它会在以上每个阶段完成前清空 micro-task 队列,下图中的 Tick 就代表了 micro-task
setTimeout(() => {
console.log("timer21");
}, 0);
Promise.resolve().then(function () {
console.log("promise1");
});
对于以上代码来说,其实和浏览器中的输出是一样的,micro-task 永远执行在 macro-task 前面。
最后我们来讲讲 Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 micro-task 执行。
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function () {
console.log("promise1");
});
}, 0);
process.nextTick(() => {
console.log("nextTick");
process.nextTick(() => {
console.log("nextTick");
process.nextTick(() => {
console.log("nextTick");
process.nextTick(() => {
console.log("nextTick");
});
});
});
});
对于以上代码,大家可以发现无论如何,永远都是先把 nextTick 全部打印出来。
六个阶段
其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
从上图中,大致看出 node 中的事件循环的顺序:
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...
- timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调
- I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
- idle, prepare 阶段:仅 node 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
- check 阶段:执行 setImmediate() 的回调
- close callbacks 阶段:执行 socket 的 close 事件回调
注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)
注意点
(1) setTimeout 和 setImmediate
二者非常相似,区别主要在于调用时机不同。
- setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;
- setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行
setTimeout(function timeout() {
console.log("timeout");
}, 0);
setImmediate(function immediate() {
console.log("immediate");
});
- 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。
- 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
- 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了
但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
});
// immediate
// timeout
在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。
(2) process.nextTick
这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 micro-task 执行。
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function () {
console.log("promise1");
});
}, 0);
process.nextTick(() => {
console.log("nextTick");
process.nextTick(() => {
console.log("nextTick");
process.nextTick(() => {
console.log("nextTick");
process.nextTick(() => {
console.log("nextTick");
});
});
});
});
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
Node 与浏览器的 Event Loop 差异
浏览器和 Node 环境下,micro-task 任务队列的执行时机不同
- Node 端,micro-task 在事件循环的各个阶段之间执行
- 浏览器端,micro-task 在事件循环的 macro-task 执行完之后执行
接下我们通过一个例子来说明两者区别:
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function () {
console.log("promise1");
});
}, 0);
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function () {
console.log("promise2");
});
}, 0);
浏览器端运行结果:timer1 => promise1 => timer2 => promise2
Node 端运行结果分两种情况:
如果是 node11 版本一旦执行一个阶段里的一个宏任务 (setTimeout,setInterval 和 setImmediate) 就立刻执行微任务队列,这就跟浏览器端运行一致,最后的结果为
timer1=>promise1=>timer2=>promise2如果是 node10 及其之前版本:要看第一个定时器执行完,第二个定时器是否在完成队列中。
如果是第二个定时器还未在完成队列中,最后的结果为
timer1=>promise1=>timer2=>promise2如果是第二个定时器已经在完成队列中,则最后的结果为
timer1=>timer2=>promise1=>promise2(下文过程解释基于这种情况下)1.全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;
2.首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 micro-task 队列,同样的步骤执行 timer2,打印 timer2;
3.至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 micro-task 队列的所有任务,依次打印 promise1、promise2
Node 端的处理过程如下:
关于 setTimeOut、setImmediate、process.nextTick()的比较
setTimeout()
将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。 当主线程时间执行过长,无法保证回调会在事件指定的时间执行。浏览器端每次 setTimeout 会有 4ms 的延迟,当连续执行多个 setTimeout,有可能会阻塞进程,造成性能问题。
setImmediate()
服务端 node 提供的方法。事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行。和 setTimeout(fn,0)的效果差不多。
process.nextTick()
服务器端 node 提供的方法。插入到事件队列尾部,但在下次事件队列之前会执行。它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。
大致流程:当前”执行栈”的尾部–>下一次 Event Loop(主线程读取”任务队列”)之前–>触发 process 指定的回调函数。
用此方法可以用于处于异步延迟的问题。
setTimeout 和 promise 的区别?宏任务和微任务是什么?有什么区别?
宏任务队列可以有多个,微任务队列只有一个。
宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering. 微任务:process.nextTick, Promise, Object.observer, MutationObserver.
取一个宏任务来执行。执行完毕后,下一步。 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。 更新 UI 渲染。
写出下面的执行结果:
console.log("1");
setTimeout(function () {
console.log("2");
new Promise(function (resolve) {
console.log("3");
resolve();
}).then(function () {
console.log("4");
});
});
new Promise(function (resolve) {
console.log("5");
resolve();
}).then(function () {
console.log("6");
});
setTimeout(function () {
console.log("7");
new Promise(function (resolve) {
console.log("8");
resolve();
}).then(function () {
console.log("9");
});
});
结果是: 1 5 6 2 3 4 7 8 9
setTimeout、Promise、Async/Await 的区别
主要是考察这三者在事件循环中的区别,事件循环中分为宏任务队列和微任务队列。 其中 setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行; promise.then 里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;async 函数表示函数里面可能会有异步方法,await 后面跟一个表达式,async 方法执行时,遇到 await 会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。
1. setTimeout
console.log("script start"); //1. 打印 script start
setTimeout(function () {
console.log("setTimeout"); // 4. 打印 setTimeout
}); // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log("script end"); //3. 打印 script start
// 输出顺序:script start->script end->setTimeout
2. Promise
Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。
console.log("script start");
let promise1 = new Promise(function (resolve) {
console.log("promise1");
resolve();
console.log("promise1 end");
}).then(function () {
console.log("promise2");
});
setTimeout(function () {
console.log("setTimeout");
});
console.log("script end");
// 输出顺序: script start->promise1->promise1 end->script end->promise2->setTimeout
当 JS 主线程执行到 Promise 对象时,
- promise1.then() 的回调就是一个 task
- promise1 是 resolved 或 rejected: 那这个 task 就会放入当前事件循环回合的 micro-task queue
- promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 micro-task queue 中
- setTimeout 的回调也是个 task ,它会被放入 macro-task queue 即使是 0ms 的情况
3. async/await
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
async1();
console.log("script end");
// 输出顺序:script start->async1 start->async2->script end->async1 end
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
举个例子:
async function func1() {
return 1;
}
console.log(func1());
很显然,func1 的运行结果其实就是一个 Promise 对象。因此我们也可以使用 then 来处理后续逻辑。
func1().then((res) => {
console.log(res); // 30
});
await 的含义为等待,也就是 async 函数需要等待 await 后的函数执行完成并且有了返回结果(Promise 对象)之后,才能继续执行下面的代码。await 通过返回一个 Promise 对象来实现同步的效果。
- setTimeout 方法用于在指定的毫秒数后调用函数或计算表达式。setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间,setTimeout() 的第二个参数的最小值不得小于 4 毫秒,如果低于这个值,则默认是 4 毫秒
- 1.setTimeout 它会开启一个定时器线程,并不会影响后续的代码执行,这个定时器线程会在事件队列后面添加一个任务, 当 js 线程在主线程执行其他线程代码完毕后,就会取出事件队列中的事件进行执行,
- 2.定时器中的 this 存在隐式丢失的情况
var a = 0;
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
setTimeout(obj.foo, 100); //0
若想获得 obj 对象中的 a 属性值,可以将 obj.foo 函数放置在定时器中的匿名函数中进行隐式绑定
var a = 0;
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
setTimeout(function () {
obj.foo();
}, 100); //2
或者使用 bind 方法将 foo()方法的 this 绑定到 obj 上,call,apply 方法均可
var a = 0;
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
setTimeout(obj.foo.bind(obj), 100); //2
- promise 就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数 Promise 新建后立即执行,promise 提供 Promise.all,promise.race,promise.resolve,promise.reject 等方法
- async/await
- async/await 是写异步代码的新方式,以前的方法有回调函数和 Promise。
- async/await 是基于 Promise 实现的,它不能用于普通的回调函数。
- async/await 与 Promise 一样,是非阻塞的。
- async/await 使得异步代码看起来像同步代码,这正是它的魔力所在
不同点:
then 和 setTimeout 执行顺序,即 setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.then()在本轮“事件循环”结束时执行。因此 then 函数先输出,setTimeout 后输出。 例子:
var p1 = new Promise(function (resolve, reject) {
resolve(1);
});
setTimeout(function () {
console.log("will be executed at the top of the next Event Loop");
}, 0);
p1.then(function (value) {
console.log("p1 fulfilled");
});
setTimeout(function () {
console.log("will be executed at the bottom of the next Event Loop");
}, 0);
// p1 fulfilled
// will be executed at the top of the next Event Loop
// will be executed at the bottom of the next Event Loop
原因:
JavaScript 将异步任务分为 macro-task 和 micro-task,
macro-task 包含 macro-task Queue(宏任务队列)主要包括 setTimeout,setInterval, setImmediate, requestAnimationFrame, NodeJS 中的 I/O 等。
micro-task 包含独立回调 micro-task:如 Promise,其成功/失败回调函数相互独立;复合回调 micro-task:如 Object.observe, MutationObserver 和 NodeJs 中的 process.nextTick ,不同状态回调在同一函数体;
js 执行顺序
依次执行同步代码直至执行完毕;
检查 macro-task 队列,若有触发的异步任务,则取第一个并调用其事件处理函数,然后跳至第三步,若没有需处理的异步任务,则直接跳至第三步;
检查 micro-task 队列,然后执行所有已触发的异步任务,依次执行事件处理函数,直至执行完毕,然后跳至第二步,若没有需处理的异步任务中,则直接返回第二步,依次>执行后续步骤;
最后返回第二步,继续检查 macro-task 队列,依次执行后续步骤;
如此往复,若所有异步任务处理完成,则结束;
JS 垃圾回收机制
垃圾收集机制的原理:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
在 JS 中,有两种内存回收算法。第一种是引用计数垃圾收集,第二种是标记-清除算法(从 2012 年起,所有现代浏览器都使用了标记-清除垃圾回收算法)。
1. 引用计数垃圾收集
如果一个对象没有被其他对象引用,那它将被垃圾回收机制回收。
let o = { a: 1 };
一个对象被创建,并被 o 引用。
o = null;
刚才被 o 引用的对象现在是零引用,将会被回收。
循环引用
引用计数垃圾收集有一个缺点,就是循环引用会造成对象无法被回收。
function f() {
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
在 f() 执行后,函数的局部变量已经没用了,一般来说,这些局部变量都会被回收。但上述例子中,o 和 o2 形成了循环引用,导致无法被回收。
引用:在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个 Javascript 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
引用计数:此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
限制:循环引用,如果两个对象相互引用,那么垃圾回收机制无法判定对象不再需要,因此这部分内存并不会被回收。DOM 中同样存在循环引用的情况,也会造成内存泄漏。
定义和用法:引用计数是跟踪记录每个值被引用的次数。
基本原理:就是变量的引用次数,被引用一次则加 1,当这个引用计数为 0 时,被视为准备回收的对象。
流程:
- 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值引用次数就是 1
- 同一个值又被赋值另一个变量,这个引用类型的值引用次数加 1
- 当包含这个引用类型值得变量又被赋值另一个值了,那么这个引用类型的值的引用次数减 1
- 当引用次数变成 0 时, 说明这个值需要解除引用
- 当垃圾回收机制下次运行时,它就会释放引用次数为 0 的值所占用的内存
2. 标记-清除算法
这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
对于刚才的例子来说,在 f() 执行后,由于 o 和 o2 从全局对象出发无法获取到,所以它们将会被回收。
标记清除算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫根的对象(在 JavaScript 中,根是全局对象)。垃圾回收器将定期从根开始,找到所有从根引用的对象,然后找到这些对象引用的对象......从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
定义和用法:
当变量进入环境时,将变量标记"进入环境",当变量离开环境时,标记为:"离开环境"。某一个时刻,垃圾回收器会过滤掉环境中的变量,以及被环境变量引用的变量,剩下的就是被视为准备回收的变量。
到目前为止,IE、Firefox、Opera、Chrome、Safari 的 js 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
流程:
- 浏览器在运行的时候会给存储再内存中的所有变量都加上标记
- 去掉环境中的变量以及被环境中引用的变量的标记
- 如果还有变量有标记,就会被视为准备删除的变量
- 垃圾回收机制完成内存的清除工作,销毁那些带标记的变量,并回收他们所占用的内存空间
高效使用内存
在 JS 中能形成作用域的有函数、全局作用域、with,在 es6 还有块作用域。局部变量随着函数作用域销毁而被释放,全局作用域需要进程退出才能释放或者使用 delete 和赋空值 null undefined。
在 V8 中用 delete 删除对象可能会干扰 V8 的优化,所以最好通过赋值方式解除引用。